該系列是為了讓看過Vue官方文件或學過Vue但是卻不知道怎麼下手去重構現在有的網站而去規畫的系列文章,在這邊整理了許多我自己使用Vue重構很多網站的經驗分享給讀者們。
我們很常會再 vue 的 template 中使用 v-if
或 v-show
等語法來處理我們的 ui ,但其實 vue 有提供我們一個 API 叫做directive
讓我們可以自己定義模板語法,我們就會使用 directive
來把一些對 ui 的操作給包起來,方便我們使用。
我們可以看一下官方所提供的這個例子
<div id="simplest" class="demo">
<input v-focus />
</div>
const app = Vue.createApp({});
app.directive('focus', {
mounted(el) {
el.focus()
}
})
app.mount('#simplest')
在這邊我對 directive
去定義一個叫做 focus
的語法,然後塞入我們的 vue 裡面,這樣一來我再 template 中就可以用 v-[name]
的方式再 html 使用,然後達到我要的效果,所以這邊我用 v-focus
然後綁定再 input
這個 DOM 身上。
再來我們來看 directive
的第二個參數,這裡面塞的就是我們的生命週期函式。
以下是我們 directive
的生命週期表,在這邊可以看到跟我們的 component 的生命週期很像,但是我們是以掛到DOM上面來作為執行的順序。
官方文件: https://v3.cn.vuejs.org/api/application-api.html#directive
// Vue3 版本
app.directive('my-directive', {
// 在綁定DOM的 attribute 或事件監聽被使用之前調用
created() {},
// 在綁定DOM的父組件掛載之前調用
beforeMount() {},
// 綁定DOM的父組件被掛載時調用
mounted() {},
// 在包含組件的 VNode 更新之前調用
beforeUpdate() {},
// 在包含組件的 VNode 及其子組件的 VNode 更新之後調用
updated() {},
// 在綁定DOM的父組件移除之前調用
beforeUnmount() {},
// 移除綁定DOM的父組件時調用
unmounted() {}
})
vue2 directive
的生命週期函式已經被重命命名,所以再升級 vue3 的時候要特別注意一下。
// Vue2 版本
Vue.directive('highlight', {
// 綁定到DOM後調用。只調用一次。
bind() {},
// DOM插入父組件後調用。
inserted () {},
// 當DOM更新,但子組件尚未更新時調用。
update () {},
// 組件和子組件被更新,就會調用。
componentUpdated () {},
// 指令被移除就會調用,也只調用一次。
unbind () {},
})
官方文件 : https://v3.vuejs.org/guide/migration/custom-directives.html#overview
我們的 mounted
會把它掛載的 DOM 實體給 return
回來,這裡回傳是一個 input 的表單DOM,所以我們就可以像一般的 javascript 操作一樣,我執行這個.focus()
的函式,讓我一進來這個頁面的時候,將我們滑鼠目標放到這個表單身上。
app.directive('focus', {
mounted(el) {
el.focus()
}
})
官方的 codepen 範例 : https://codepen.io/team/Vue/pen/JjdxaJW?editors=1010
directive
有了概念之後,現在我們來看一些更加實際的使用方式首先這是一個像是FB貼文的卡片,然後當我拿到資料之後,我就會把內容給一個擺上去,但是你注意看一下時間的地方,下面是API 回傳的格式。
{
"createdAt": "2021-09-14T04:28:05.885Z",
"name": "Phyllis Abernathy V",
"avatar": "https://cdn.fakercloud.com/avatars/amanruzaini_128.jpg",
"post_date": 1631608299879,
"photo": "http://placeimg.com/640/480",
"content": "transmit cross-platform capacitor",
"id": "1"
},
一切都看起來蠻正常的,但是在時間的地方它是回傳一個 Timestamp ( milliseconds )
給你,這其實蠻常見的,不是所有的後端都會幫你轉換時間的部分,所以這時候我們放上去卡片的地方就要寫一個 function 去做轉換時間的動作,在這邊我選擇用 day.js
來幫助我轉換時間,體積小功能又強大,沒用過的可以考慮用用看。
Day.js 官網 : https://day.js.org/en/
我們可以用以下的方式來轉換時間格式,所以接下來包裝一下。
dayjs(1631608299879).format('YYYY/MM/DD')
<script>
import { ref, onMounted } from "vue";
import axios from "axios";
import dayjs from "dayjs";
export default {
setup() {
const postCard = ref([]);
const timestamp = (time) =>{
return dayjs(time).format('YYYY/MM/DD')
}
onMounted(() => {
axios.get("https://60bd9841ace4d50017aab3ec.mockapi.io/api/post_card").then((res) => {
postCard.value = res.data;
});
});
return {
postCard,
timestamp
};
},
};
</script>
<template>
<div class="card" v-for="card in postCard" :key="card.id">
<header>
<img class="avatar" :src="card.avatar" />
<div>
<h1>{{ card.name }}</h1>
<p>
{{ timestamp(card.post_date) }}
</p>
</div>
</header>
<p class="content">{{ card.content }}</p>
<img class="post_photo" :src="card.photo" alt="" />
</div>
</template>
我寫了一個 function 會回傳 format 之後的時間,然後放到 html 之中timestamp(card.post_date)
給 render 出來,這樣可以把我們的時間做轉換。
如果很多地方都要做這樣的時間轉換,然後我又要一直寫 dayjs(time).format('YYYY/MM/DD')
,看起來就不是很理想,所以這邊我要使用 directive
來包裝這個 dayjs
的 format
函式。
首先我的需求是要像下面這樣,我只要使用 v-timeformat
就可以把 timestamp 給轉換成我要的格式給我。
<p v-timeformat="card.post_date"></p>
所以接下來我們來註冊一個 timeformat 的模板語法。
import { createApp } from "vue";
import dayjs from "dayjs";
import App from "./App.vue";
const app = createApp(App);
// 先註冊一個 timeformat 的語法
app.directive("timeformat", {
mounted(el, binding) {
const time = dayjs(binding.value).format("YYYY年MM月DD日");
el.innerText = time;
}
});
app.mount("#app");
因為再 template 這邊我有塞內容 (card.post_date
) 進去
<template>
<div class="card" v-for="card in postCard" :key="card.id">
<header>
<img class="avatar" :src="card.avatar" />
<div>
<h1>{{ card.name }}</h1>
<p v-timeformat="card.post_date"></p>
</div>
</header>
<p class="content">{{ card.content }}</p>
<img class="post_photo" :src="card.photo" alt="" />
</div>
</template>
所以 mounted
除了回傳綁定的 DOM 以外,第二個參數會回傳你傳入的 value
,所以我們可以 binding.value
的方式取得它的值,當我拿到 value
之後我就執行 dayjs(binding.value).format("YYYY年MM月DD日")
,把我的時間給轉換完後,再透過 innerText
給塞入到我們的 DOM 之中,就大功告成了。
codesandbox 完整範例 : https://codesandbox.io/s/zv46o?file=/src/main.js
directive
的另外一種使用方式我們很常會製作有很多圖片的頁面,但是圖片的載入是非同步的,所以通常我們會寫一個 load 的 function 來判斷圖片有沒有載入完成,在我們前面的範例也有示範相關的做法,不過那種做法通常都是圖片前面會蓋一個 laoding 的頁面,等到圖片載入完成之後再拿掉 laoding page,這種作法雖然很常見,不過今天我們來介紹一下如何使用 directive
來做到像是圖片的 lazyload 效果。
首先我希望我的 img 身上可以掛一個 v-src
的語法
<img v-src="https://cdn.fakercloud.com/avatars/popey_128.jpg" />
然後它可以被在背後去載入,載入完成後把圖片給放到 img 標籤身上給 show 出來。
app.directive("src", (el, binding) => {
});
我們先註冊一個名叫 src
的 directive
,然後第二個參數給它一個 callback 函式,callback 函式???,你可能會問說怎麼不是一個物件,directive
的第二個參數總共可以帶兩種不同類型的參數,一個是物件
,一個是函式
,物件就像我們上面一樣,可以有多個生命週期,如果是一個函式的話,這個函式等同於 mounted
的階段執行,然後一樣可以回傳綁定的 DOM 還有 value。
app.directive("src", (el, binding) => {
el.style.opacity = 0;
if (binding.value) {
const img = new Image();
img.src = binding.value;
img.onload = () => {
el.src = binding.value;
el.style.opacity = 1;
};
}
});
首先一開始的時候,我先把圖片的透明度設成 0
,你可能會問說,怎麼不是 display: none
而是用 opacity = 0
呢 ? 原因是因為我的圖片如果有設css的高度的話,設定透明度高度還會在,所以我的版型還不會跑版,再來判斷我的 binding.value
是否有將圖片的路徑給傳入進來,如果有傳入圖片路徑我就去對它執行 onload
看圖片有沒有載入完成,如果載入完成我就把圖片的透明度給變成 1
,也就是opacity = 1
,這時候可以做一下透明度 0 ~ 1
的補間動態,所以加一下 css3 的 transition
就大功告成。
<style>
img {
transition: opacity 0.3s;
}
</style>
這個時候我就會加一個 onerror
來處理有問題的圖片,我推薦像是下面這種作法。
import errorImg from "./assets/error.png";
app.directive("src", (el, binding) => {
el.style.opacity = 0;
if (binding.value) {
const img = new Image();
img.src = binding.value;
img.onload = () => {
el.src = binding.value;
el.style.opacity = 1;
};
img.onerror = () => {
el.src = errorImg;
el.style.opacity = 1;
};
}
});
透過 onerror
我們可以確保當圖片出現問題的時候,不會讓畫面直接出現問題,而是我們可以載入預設的圖片,讓畫面至少維持在一個狀態,不會看起來很突兀,所以我先載入了一個 error.png
,當圖片出現問題的時候,我就把這個圖片給替換上去,透明度改回 1
,這樣一來畫面上面就可以看到我們預先準備好的圖片了。
codesandbox 完整範例 : https://codesandbox.io/s/oulpd?file=/src/main.js
所以在開發上面不是所有的共用邏輯都需要使用 composition api
,我們可以依照自己的需求來決定使用那些功能來幫助我們開發,像是把第三方套件結合自己定義的模板語法來產生最後結果的做法,或是自己做一個 lazyload
的效果也是一種不錯的選擇,當然能用 directive
的地方還有很多! 不過今天就先到一段落吧,我們明天見囉。
Ps. 購買的時候請登入或註冊該平台的會員,然後再使用下面連結進入網站點擊「立即購課」,這樣才可以讓我獲得更多的課程分潤,還可以幫助我完成更多豐富的內容給各位。
我有開設了一堂專門針對Vue3從零開始教學的課程,如果你覺得不錯的話,可以購買我課程來學習
https://hiskio.com/bundles/9WwPNYRpz?s=tc
那如果對於JS基礎不熟的朋友,我也有開設JS的入門課程,可以參考這個課程
https://hiskio.com/bundles/b9Rovqy7z?s=tc
Mike 的 Youtube 頻道
Mike的medium
MIke 的官方 line 帳號,好友搜尋 @mike_cheng